/*
 * Copyright (C) 2007, 2008, 2010, 2020 XStream Committers.
 * All rights reserved.
 *
 * The software in this package is published under the terms of the BSD
 * style license a copy of which has been included with this distribution in
 * the LICENSE.txt file.
 * 
 * Created on 13. September 2007 by Joerg Schaible.
 */

package com.feilong.lib.xstream.core.util;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PushbackInputStream;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Map;

/**
 * A {@link Reader} that evaluates the XML header. It selects its encoding based on the encoding read with the XML
 * header of the provided {@link InputStream}. The default encoding is <em>UTF-8</em> and the version is 1.0 if the
 * stream does not contain an XML header or the attributes are not set within the header.
 * 
 * @author J&ouml;rg Schaible
 * @since 1.3
 */
public final class XmlHeaderAwareReader extends Reader{

    private final InputStreamReader reader;

    private final double            version;

    private static final String     KEY_ENCODING           = "encoding";

    private static final String     KEY_VERSION            = "version";

    private static final String     XML_TOKEN              = "?xml";

    private static final int        STATE_BOM              = 0;

    private static final int        STATE_START            = 1;

    private static final int        STATE_AWAIT_XML_HEADER = 2;

    private static final int        STATE_ATTR_NAME        = 3;

    private static final int        STATE_ATTR_VALUE       = 4;

    /**
     * Constructs an XmlHeaderAwareReader.
     * 
     * @param in
     *            the {@link InputStream}
     * @throws UnsupportedEncodingException
     *             if the encoding is not supported
     * @throws IOException
     *             occurred while reading the XML header
     * @since 1.3
     */
    public XmlHeaderAwareReader(final InputStream in) throws UnsupportedEncodingException,IOException{
        final PushbackInputStream[] pin = new PushbackInputStream[] {
                                                                      in instanceof PushbackInputStream ? (PushbackInputStream) in
                                                                                      : new PushbackInputStream(in, 64) };
        final Map header = getHeader(pin);
        version = Double.parseDouble((String) header.get(KEY_VERSION));
        reader = new InputStreamReader(pin[0], (String) header.get(KEY_ENCODING));
    }

    private Map getHeader(final PushbackInputStream[] in) throws IOException{
        final Map header = new HashMap();
        header.put(KEY_ENCODING, "UTF-8");
        header.put(KEY_VERSION, "1.0");

        int state = STATE_BOM;
        final ByteArrayOutputStream out = new ByteArrayOutputStream(64);
        int i = 0;
        char ch = 0;
        char valueEnd = 0;
        final StringBuffer name = new StringBuffer();
        final StringBuffer value = new StringBuffer();
        boolean escape = false;
        while (i != -1 && (i = in[0].read()) != -1){
            out.write(i);
            ch = (char) i;
            switch (state) {
                case STATE_BOM:
                    if ((ch == 0xEF && out.size() == 1) || (ch == 0xBB && out.size() == 2) || (ch == 0xBF && out.size() == 3)){
                        if (ch == 0xBF){
                            out.reset();
                            state = STATE_START;
                        }
                        break;
                    }else if (out.size() > 1){
                        i = -1;
                        break;
                    }else{
                        state = STATE_START;
                    }
                    // fall through
                case STATE_START:
                    if (!Character.isWhitespace(ch)){
                        if (ch == '<'){
                            state = STATE_AWAIT_XML_HEADER;
                        }else{
                            i = -1;
                        }
                    }
                    break;
                case STATE_AWAIT_XML_HEADER:
                    if (!Character.isWhitespace(ch)){
                        name.append(Character.toLowerCase(ch));
                        if (!XML_TOKEN.startsWith(name.substring(0))){
                            i = -1;
                        }
                    }else{
                        if (name.toString().equals(XML_TOKEN)){
                            state = STATE_ATTR_NAME;
                            name.setLength(0);
                        }else{
                            i = -1;
                        }
                    }
                    break;
                case STATE_ATTR_NAME:
                    if (!Character.isWhitespace(ch)){
                        if (ch == '='){
                            state = STATE_ATTR_VALUE;
                        }else{
                            ch = Character.toLowerCase(ch);
                            if (Character.isLetter(ch)){
                                name.append(ch);
                            }else{
                                i = -1;
                            }
                        }
                    }else if (name.length() > 0){
                        i = -1;
                    }
                    break;
                case STATE_ATTR_VALUE:
                    if (valueEnd == 0){
                        if (ch == '"' || ch == '\''){
                            valueEnd = ch;
                        }else{
                            i = -1;
                        }
                    }else{
                        if (ch == '\\' && !escape){
                            escape = true;
                            break;
                        }
                        if (ch == valueEnd && !escape){
                            valueEnd = 0;
                            state = STATE_ATTR_NAME;
                            header.put(name.toString(), value.toString());
                            name.setLength(0);
                            value.setLength(0);
                        }else{
                            escape = false;
                            if (ch != '\n'){
                                value.append(ch);
                            }else{
                                i = -1;
                            }
                        }
                    }
                    break;
            }
        }

        byte[] pushbackData = out.toByteArray();
        for (i = pushbackData.length; i-- > 0;){
            final byte b = pushbackData[i];
            try{
                in[0].unread(b);
            }catch (IOException ex){
                in[0] = new PushbackInputStream(in[0], ++i);
            }
        }
        return header;
    }

    /**
     * @see InputStreamReader#getEncoding()
     * @since 1.3
     */
    public String getEncoding(){
        return reader.getEncoding();
    }

    /**
     * @see InputStreamReader#getEncoding()
     * @since 1.3
     */
    public double getVersion(){
        return version;
    }

    /**
     * @see java.io.Reader#mark(int)
     */
    @Override
    public void mark(final int readAheadLimit) throws IOException{
        reader.mark(readAheadLimit);
    }

    /**
     * @see java.io.Reader#markSupported()
     */
    @Override
    public boolean markSupported(){
        return reader.markSupported();
    }

    /**
     * @see java.io.Reader#read()
     */
    @Override
    public int read() throws IOException{
        return reader.read();
    }

    /**
     * @see java.io.Reader#read(char[], int, int)
     */
    @Override
    public int read(final char[] cbuf,final int offset,final int length) throws IOException{
        return reader.read(cbuf, offset, length);
    }

    /**
     * @see java.io.Reader#read(char[])
     */
    @Override
    public int read(final char[] cbuf) throws IOException{
        return reader.read(cbuf);
    }

    // TODO: This is JDK 1.5    
    //    public int read(final CharBuffer target) throws IOException {
    //        return reader.read(target);
    //    }

    /**
     * @see java.io.Reader#ready()
     */
    @Override
    public boolean ready() throws IOException{
        return reader.ready();
    }

    /**
     * @see java.io.Reader#reset()
     */
    @Override
    public void reset() throws IOException{
        reader.reset();
    }

    /**
     * @see java.io.Reader#skip(long)
     */
    @Override
    public long skip(final long n) throws IOException{
        return reader.skip(n);
    }

    /**
     * @see java.io.Reader#close()
     */
    @Override
    public void close() throws IOException{
        reader.close();
    }

    /**
     * @see java.lang.Object#equals(java.lang.Object)
     */
    @Override
    public boolean equals(final Object obj){
        return reader.equals(obj);
    }

    /**
     * @see java.lang.Object#hashCode()
     */
    @Override
    public int hashCode(){
        return reader.hashCode();
    }

    /**
     * @see java.lang.Object#toString()
     */
    @Override
    public String toString(){
        return reader.toString();
    }
}
